package socket;

import java.io.*;
import java.lang.reflect.Array;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

/**
 * 聊天室服务端
 */
public class Server {
    /**
     * 运行在服务端的ServerSocket主要有两个工作:
     * 1:向系统申请服务端口,客户端就是通过这个端口与服务端建立连接的
     * 2:监听服务端口,一旦一个客户端通过该端口建立连接,服务端会自动创建一个Socket
     * 通过这个Socket与该客户交互
     * <p>
     * <p>
     * 我们可以吧Socket比喻成一个电话,ServerSocket则相当于某呼叫中心的"总机"
     * 下面连着一堆电话.
     */
    private ServerSocket server;
    /*
        该数组用于保存所有ClientHandler用于给客户端发现消息的输出流
        便于所有ClientHandler获取到其他ClientHandler的输出流来广播消息

        注:由于Server是ClientHandler的外部类,而内部类可以访问外部类的成员
            因此在Server上定于的数组可以被所有ClientHandler访问到,从而作为
            共享数据使用
     */
    //private PrintWriter[] allOut = {};
    private Collection<PrintWriter> allOut = new ArrayList<>();
    /*
        构造方法,初始化服务端
     */
    public Server() {
        try {
            /*
                实例化ServerSocket的同时需要指定服务端口
                服务端就是通过这个端口与服务端建立连接的.
                指定的端口号不能与其他应用程序申请的一样,否则系统会抛出一个异常
                java.net.BindException:Address already in use
             */

            System.out.println("正在启动服务端-------");
            server = new ServerSocket(8088);
            System.out.println("服务端启动完毕!!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /*
        启动程序
     */
    public void start() {
        /*
            ServerSocket提供了接受客户端连接的方法:
            Socket accept()
            该方法是一个阻塞方法,调用后服务端会卡住,此时开始等待客户端的连接,
            一旦一个客户端创建Socket并与服务端建立连接,那么这个方法会立即返回一个
            Socket,通过这个Socket就可以和客户端的Socket配对并进行双向通讯了
            查IP地址   ipconfig
         */
        try {
            while (true) {
                System.out.println("等待客户端连接---");
                Socket socket = server.accept();
                System.out.println("一个客户端连接了!");
                /*
                    启动一个线程,处理与该客户端的交互
                    使得主线程将该线程启动后可以再次循环调用
                    上面的accept方法等待其他客户端的连接
                */
                //实例化线程任务
                ClientHandler handler = new ClientHandler(socket);
                //创建线程执行该任务
                Thread t = new Thread(handler);
                t.start();//启动线程处理该客户端交互
            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    //内部类.  该线程任务是负责与指定的客户端进行交互
    private class ClientHandler implements Runnable {
        private Socket socket;
        private String host;//记录客户端IP地址信息

        public ClientHandler(Socket socket) {
            this.socket = socket;
            //通过socket获取远端计算机IP地址的字符串格式
            host = socket.getInetAddress().getHostAddress();

        }

        public void run() {
            PrintWriter pw = null;
            try {
                 /*
                Socket提供的方法:
                InputStream getInputStream()
                通过Socket获取的输入流,使用它可以读取远端计算机发送过来的字节.
             */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in);
                BufferedReader br = new BufferedReader(isr);
                OutputStream ou = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(ou,"UTF-8");
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw,true);
                //将该pw放入allOut数组,供其他ClientHandler使用以便广播消息
                //synchronized (this) {不行
                //锁临界资源,抢谁锁谁.百分之九十九都可以
                //synchronized (allOut) {但是在这里不行,因为这里数组缩容,创建了新的数组对象,所以不可以
                //synchronized (server) {//共有的不会改变的对象就可以
                /*
                    如果allOut是一个集合,那么它可以作为锁对象使用,因为无论
                    是添加元素还是删除元素,并不会创建一个新的集合,所以这个
                    集合对象本身没有发生改变.
                 */
                synchronized (allOut) {
                    //1扩容allOut
                   // allOut = Arrays.copyOf(allOut, allOut.length + 1);

                    //2将pw放入数组最后一个位置
                    //allOut[allOut.length - 1] = pw;
                    allOut.add(pw);
                }
                //广播该用户上线了
                sendMessage(host+"上线了!当前在线人数:"+allOut.size());




                String line;
            /*
                这里使用BufferedReader按行读取客户端发送过来的每一行字符串
                时,如果客户端断开连接(客户端正常调用socket.close())那么
                readLine方法会返回null,说明流读取到了末尾.
                如果客户端非正常断开(没有调用socket.close()或强转结束程序)
                则这里readLine方法会抛出异常:
                java.net.SocketException:Connection reset
             */
                while ((line = br.readLine()) != null) {
                    System.out.println(host + ":" + line);
                    //将消息发送给所有客户端
                    sendMessage(host + ":" + line);

                }
            } catch (IOException e) {

            }finally {
               //处理客户端断开连接后的操作

               //将对当前客户端发送消息的输出流从allOut数组中删除
//                for (int i = 0; i < allOut.length-1; i++) {
//                    if (allOut[i]==pw){
//                        allOut[i] = allOut[allOut.length-1];
//                        allOut = Arrays.copyOf(allOut,allOut.length-1);
//                        break;
//                    }
//                }
//                Iterator<PrintWriter> it = allOut.iterator();
//                while (it.hasNext()){
//                    PrintWriter str  = it.next();
//                    if(pw.equals(str)){
//                        it.remove();
//                    }
//                }
                synchronized (allOut) {
                    allOut.remove(pw);

                }
                sendMessage(host+"下线了!当前在线人数:"+allOut.size());

                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         * 将给定消息广播给所有客户端
         * @param message
         */
       public void sendMessage(String message){
           //将消息发送给所有客户端
//           for (int i=0;i<allOut.length;i++) {
//               allOut[i].println(message);
//           }
           synchronized (allOut) {
               for (PrintWriter out : allOut) {
                   out.println(message);
               }
           }
           //allOut.forEach(out ->out.println(message));
           //allOut.forEach(out ->{
           // out.println(message)
           // });
           //增强型for循环的lambda写法
       }
    }
}
